package reflectUtil;


import org.jdom.Document;
import org.jdom.Element;

import javax.print.Doc;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.Map;

public class ReflectionUtil {
    /**
     * A recursive method that searches a method by name regardless of its visibility throughout the inheritance hierarchy
     * of a class.
     * Will finnaly throw a NoSuchMethodException if none of the classes got this method.
     *
     * @param aClass
     * @param name
     * @param paraTypes varargs that indicates the type(s) of the parameter(s) for the method
     * @return
     * @throws NoSuchMethodException
     */
    public static Method getSupportedMethod(Class aClass, String name, Class<?>... paraTypes) throws NoSuchMethodException {
        if (null == aClass) {
            throw new NoSuchMethodException();
        }
        try {
            return aClass.getDeclaredMethod(name, paraTypes);
        } catch (NoSuchMethodException e) {
            return getSupportedMethod(aClass.getSuperclass(), name, paraTypes);
        }
    }

    public Field[] getAllFields(Object obj) {
        Class cls = obj.getClass();
        LinkedList<Field> allFields = new LinkedList<>();
        while (cls != null) {
            for (Field f : cls.getDeclaredFields()) {
                allFields.add(f);
            }
            cls = cls.getSuperclass();
        }
        return (Field[]) allFields.toArray();
    }

    public static Field[] getAllInstanceFields(Class cls) {

        LinkedList<Field> allFields = new LinkedList<>();
        while (cls != null) {
            for (Field f : cls.getDeclaredFields()) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    allFields.add(f);
                }
            }
            cls = cls.getSuperclass();
        }
        Field[] retvalue = new Field[allFields.size()];
        return allFields.toArray(retvalue);
        /**
         * 	List accum = new LinkedList();
         * 	while (cls != null) {
         * 	    Field[] fields = cls.getDeclaredFields();
         * 	    for (int i=0; i<fields.length; i++) {
         * 		if (!Modifier.isStatic(fields[i].getModifiers())) {
         * 		    accum.add(fields[i]);
         *                }
         *        }
         * 	    cls = cls.getSuperclass();* 	}
         * 	Field[] retvalue = new Field[accum.size()];
         * 	return (Field[]) accum.toArray(retvalue);
         */
    }

    public static Field[] makeAllFieldsAccessible(Field[] fs) {
        for (int i = 0; i < fs.length; i++) {
            if (!Modifier.isPublic(fs[i].getModifiers())) {
                fs[i].setAccessible(true);
            }
        }
        return fs;
    }

    public static Document serializeObject(Object source) throws Exception {
        return serializeHelper(source,
                new Document(new Element("serialized")),
                new IdentityHashMap());
    }

    private static Document serializeHelper(Object source, Document target, Map table) throws Exception {
        String id = Integer.toString(table.size());
        table.put(source, id);
        Class sourceClz = source.getClass();
        Element oElt = new Element("Object");
        oElt.setAttribute("class", sourceClz.getName());
        oElt.setAttribute("id", id);
        target.getRootElement().addContent(oElt);

        if (!sourceClz.isArray()) {
            Field[] fields = ReflectionUtil.getAllInstanceFields(sourceClz);
            fields = ReflectionUtil.makeAllFieldsAccessible(fields);
            for (Field f : fields) {
                Element fElt = new Element("Field");
                fElt.setAttribute("name", f.getName());
                fElt.setAttribute("declaringClass", f.getDeclaringClass().getName());
                Class fieldType = f.getType();
                Object content = f.get(source);
                if (Modifier.isTransient(f.getModifiers())) {
                    content = null;
                }
                fElt.addContent(serializeVariable(fieldType, content, target, table));
                oElt.addContent(fElt);
            }

        } else {// the source class is of Array type
            Class componentType = sourceClz.getComponentType();
//            System.out.println(sourceClz.isArray());
            int length = Array.getLength(source);
            oElt.setAttribute("length", Integer.toString(length));
            for (int i = 0; i < length; i++) {
                oElt.addContent(serializeVariable(componentType, Array.get(source, i), target, table));
            }
        }
        return target;
    }

    private static Element serializeVariable(
            Class fieldType, Object variable, Document target, Map table
    ) throws Exception {
        if (variable == null) {
            return new Element("null");
        } else if (!fieldType.isPrimitive()) {
            Element reference = new Element("reference");
            if (table.containsKey(variable)) {
                reference.setText(table.get(variable).toString());
            } else {
                reference.setText(Integer.toString(table.size()));
                serializeHelper(variable, target, table);
            }
            return reference;
        } else {
            Element value = new Element("value");
            value.setText(variable.toString());
            return value;
        }

    }
}
